package cn.xzqwjw.taskmanager.utils;

import cn.xzqwjw.taskmanager.common.customEnum.ResponseCodeEnum;
import cn.xzqwjw.taskmanager.common.constant.CustomConstant;
import cn.xzqwjw.taskmanager.common.exception.CustomException;
import cn.xzqwjw.taskmanager.domain.vo.UploadConfigVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.activation.MimetypesFileTypeMap;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;

/**
 * 实现文件本地服务器上传下载功能
 * 图片文件实现添加文字或图片水印功能
 *
 * @author rush
 */
@Slf4j
public class UldlUtil {

  private final static String SEPARATOR = File.separator;

  // ==================== 上传到本地服务器 ====================

  /**
   * 上传文件到本地服务器
   *
   * @param uploadFile 上传的文件
   * @param configDto  上传文件功能的配置
   */
  public static void upload2Local(MultipartFile uploadFile, UploadConfigVo configDto) {
    save2ServerLocal(uploadFile, configDto);
  }

  // ==================== 从本地服务器下载文件 ====================

  /**
   * 根据包括物理绝对路径的完整文件名从服务器得到该文件，并以resource形式返回
   *
   * @param entireFileName 要下载的文件，包括物理绝对路径的完整文件名
   */
  public static Resource downloadAsResource(String entireFileName) {
    try {
      Path path = Path.of(entireFileName);
      UrlResource resource = new UrlResource(path.toUri());
      if (resource.exists()) {
        return resource;
      } else {
        throw new CustomException(ResponseCodeEnum.NOT_FOUND);
      }
    } catch (MalformedURLException e) {
      throw new CustomException(ResponseCodeEnum.NOT_FOUND);
    }
  }

  /**
   * 从服务器上读取文件，并以流的形式放到输入参数response中
   */
  public static void downloadAsStream(HttpServletResponse response, String entireFileName) {
    File file = new File(entireFileName);
    if (!file.exists()) {
      throw new CustomException(ResponseCodeEnum.NOT_FOUND);
    }
    /* 设置响应头
     setHeader(name, value)：
     如果Header中没有定义则添加，如果已定义则用新的value覆盖原用value值。
     addHeader(name, value)：
     如果Header中没有定义则添加，如果已定义则保持原有value不改变。
     addHeader：添加一个新的请求头字段。（一个请求头中允许有重名字段。）
     设置强制下载不打开，这是一种hack的做法，有时候会失效，不推荐使用
     response.setContentType("application/force-download");
     */
    response.setContentType(getContentType(entireFileName));
    // 设置文件大小
    response.setContentLengthLong(file.length());
    // 这里的Content-Disposition中指定的类型是文件的扩展名
    // 并且弹出的下载对话框中的文件类型图片是按照文件的扩展名显示的
    // 点保存后，文件以filename的值命名，保存类型以Content中设置的为准
    // 注意：在设置Content-Disposition头字段之前，一定要设置Content-Type头字段
    // CORS请求时，XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段：
    // Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
    // 如果想拿到其他字段，就必须在 Access-Control-Expose-Headers 里面指定
    response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
    String clientFileName = setClientFileName(file.getName());
    response.setHeader("Content-Disposition", "attachment;filename=" + clientFileName);
    // 用了 Content-Disposition 就不能用下面3个去除缓存的语句
    // 不然会发现下载功能在opera和firefox里面好好的没问题，在IE下面就是不行
    // response.setHeader("Pragma", "no-cache");
    // response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    // response.setDateHeader("Expires", 0);
    try (FileInputStream fis = new FileInputStream(entireFileName);
         BufferedInputStream bis = new BufferedInputStream(fis);
         OutputStream os = response.getOutputStream()) {
      byte[] buff = new byte[4096];
      int i = bis.read(buff);
      while (i != -1) {
        os.write(buff, 0, i);
        i = bis.read(buff);
      }
    } catch (IOException e) {
      throw new CustomException(ResponseCodeEnum.INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * 下载文件，返回 ResponseEntity<InputStreamResource>
   **/
  public static ResponseEntity<InputStreamResource> downloadAsResponseEntity(String entireFileName) {
    FileSystemResource fileResource = new FileSystemResource(entireFileName);
    if (!fileResource.exists()) {
      throw new CustomException(ResponseCodeEnum.NOT_FOUND);
    }
    HttpHeaders headers = new HttpHeaders();
    // CORS请求时，XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段：
    // Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
    // 如果想拿到其他字段，就必须在 Access-Control-Expose-Headers 里面指定。
    headers.add("Access-Control-Expose-Headers", "Content-Disposition");
    String clientFileName = setClientFileName(fileResource.getFilename());
    headers.add("Content-Disposition", "attachment;filename=" + clientFileName);
    // 用了 Content-Disposition 就不能用下面3个去除缓存的语句
    // 不然会发现下载功能在opera和firefox里面好好的没问题，在IE下面就是不行
    // headers.add("Pragma", "no-cache");
    // headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
    // headers.add("Expires", "0");
    long fileLength;
    InputStream is;
    try {
      fileLength = fileResource.contentLength();
      is = fileResource.getInputStream();
    } catch (IOException e) {
      throw new CustomException(ResponseCodeEnum.INTERNAL_SERVER_ERROR);
    }
    return ResponseEntity.ok()
        .contentType(MediaType.parseMediaType(getContentType(entireFileName)))
        .contentLength(fileLength)
        .headers(headers)
        .body(new InputStreamResource(is));
  }

  /**
   * 以流的方式显示图片文件
   *
   * @param entireFileName 要显示的文件，包括物理绝对路径的完整文件名
   */
  public static byte[] showImgAsStream(HttpServletResponse response, String entireFileName) {
    File file = new File(entireFileName);
    if (!file.exists()) {
      throw new CustomException(ResponseCodeEnum.NOT_FOUND);
    }
    response.setContentType(getContentType(entireFileName));
    // 设置response的Header
    // 禁止图像缓存
    response.setHeader("Pragma", "no-cache");
    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    response.setDateHeader("Expires", 0);
    // 设置图像大小
    response.addHeader("Content-Length", "" + file.length());
    /*
     MagicMatch match = Magic.getMagicMatch(bytes);
     String mimeType = match.getMimeType();
    */
    byte[] bytes;
    try (FileInputStream fis = new FileInputStream(entireFileName);
         OutputStream os = response.getOutputStream()) {
      bytes = new byte[fis.available()];
      int result = fis.read(bytes, 0, fis.available());
      if (result <= 0) {
        return null;
      }
      os.write(bytes);
    } catch (IOException e) {
      System.out.println("showImgAsStream()显示图片文件出错了！");
      return null;
    }
    return bytes;
  }

  // ======================== 要用到的私有方法 ========================

  /**
   * 将上传的文件保存到本地服务器，保存目录按文件扩展名分类，文件名为保存时间加本地文件名
   *
   * @param uploadFile 上传的文件
   * @param configVo   系统设置对象
   */
  private static void save2ServerLocal(MultipartFile uploadFile, UploadConfigVo configVo) {
    // 验证上传的文件是否为空
    if (uploadFile.isEmpty()) {
      configVo.setOpFlag(false);
      configVo.setOpMsg(ResponseCodeEnum.PARAM_IS_NULL);
      return;
    }
    // getOriginalFilename() 得到的是 aaa.zip 这样的文件名，不包括路径，包括扩展名
    String localFileName = uploadFile.getOriginalFilename();
    configVo.setClientFileName(localFileName);
    if (!StringUtils.hasText(configVo.getFileNameShow())) {
      configVo.setFileNameShow(configVo.getClientFileName());
    }
    // 得到上传文件的扩展名，包括"."
    String extName = FileUtil.getExtName(localFileName);
    if (StringUtils.hasLength(extName)) {
      extName = extName.toLowerCase(Locale.ROOT);
    }
    configVo.setExtName(extName);
    // 检查是允许上传的文件
    if (!checkExt(extName, configVo)) {
      configVo.setOpFlag(false);
      configVo.setOpMsg(ResponseCodeEnum.NOT_ALLOWED_FILE_TYPE);
      return;
    }
    // 检查上传的文件大小
    if (!checkFileSize(extName, uploadFile.getSize(), configVo)) {
      configVo.setOpFlag(false);
      configVo.setOpMsg(ResponseCodeEnum.NOT_ALLOWED_FILE_SIZE);
      return;
    }
    configVo.setFileSize(uploadFile.getSize());
    // 根据文件扩展名设置该类型文件的服务器端存放的相对路径
    String pathRelative = setDirByExtName(extName);
    // 创建时间相关目录
    pathRelative = setDirByNow(pathRelative);
    configVo.setPathRelative(pathRelative);
    // 根据完整路径创建目录
    String pathEntire = configVo.getPathRootPhysics() + pathRelative;
    FileUtil.createDirectory(pathEntire);
    configVo.setPathEntire(pathEntire);
    // 设置上传文件存放在服务器端的文件名
    // 先得到上传文件不包括扩展名的文件名
    // localFileName = FileUtil.getFileName(localFileName);
    // 自定义存放在服务器端的文件名，这里是根据时间得到
    String serverFileName = MyStringUtil.getFileNameByDate("", true, false);
    // 原本加上原文件的名字，但是如果原文件包括中文名会出现url乱码问题，比较麻烦，因此舍弃
    // serverFileName += "_" + localFileName;
    // 改为添加一个8位长度的 uuid 字符串
    serverFileName += "_" + MyStringUtil.getUuid().substring(0, 8);
    configVo.setServerFileName(serverFileName);
    // 设置上传文件存放在服务器端的完整路径+文件名+扩展名
    String entireServerFileName = pathEntire + serverFileName + extName;
    configVo.setServerFileNameEntire(entireServerFileName);
    // 获取文件的MimeType
    configVo.setMimeType(getContentType(entireServerFileName));
    // 将上传的文件保存到服务器路径下
    try {
      uploadFile.transferTo(new File(entireServerFileName));
      configVo.setOpFlag(true);
      configVo.setOpMsg(ResponseCodeEnum.OK_UPLOAD_FILE);
    } catch (IOException ex) {
      configVo.setOpFlag(false);
      configVo.setOpMsg(ResponseCodeEnum.ERROR_FILE_UPLOAD);
    }
    // 主文件保存成功后，如果是图像文件，则要根据配置文件，进行生成缩略图，加文字或者图片水印
    if (!FileUtil.isImageByExt(extName, configVo)) {
      return;
    }
    BufferedImage image = FileUtil.getImage(entireServerFileName);
    if (Objects.isNull(image)) {
      return;
    }
    int width = image.getWidth();
    int height = image.getHeight();
    // 只有图片宽度大于一定值 并且 高度大于一定值，才生成缩略图
    if ((width <= CustomConstant.THRESHOLD_WIDTH) && (height <= CustomConstant.THRESHOLD_HEIGHT)) {
      return;
    }
    configVo.setPicWidth(width);
    configVo.setPicHeight(height);
    // 设置服务器端生成的缩略图文件的完整文件名为“绝对路径+文件名+_tn.jpg"
    configVo.setThumb1FileNameEntire(pathEntire + serverFileName + "_tn.jpg");
    // 生成一张 360X270 的jpg格式的缩略图，文件名为原文件名后加 _tn
    if (configVo.getFlagThumb()) {
      ThumbUtil.setThumbBySize(configVo, 360, 270);
    }
    // 设置服务器端生成的带水印文件的完整文件名为“绝对路径+文件名+_wm.jpg"
    configVo.setServerFileNameEntireWithWm(pathEntire + serverFileName + "_wm.jpg");
    if (configVo.getFlagWaterMarkTxt()) {
      // 文字水印，生成一张jpg格式的图像，文件名为原文件名后加 _wm_txt
      ThumbUtil.drawWmTxt(configVo);
    } else if (configVo.getFlagWaterMarkImg()) {
      // 图片水印，生成一张jpg格式的图像，文件名为原文件名后加 _wm_img
      ThumbUtil.drawWmImg(configVo);
    }
  }

  // ======================== 要用到的通用方法 ========================

  /**
   * 设置客户端下载的文件名
   */
  private static String setClientFileName(String clientFileName) {
    if (!StringUtils.hasLength(clientFileName)) {
      clientFileName = "xzqwjw-file";
    }
    String result;
    // 为了解决中文名称乱码问题 这里是把文件名按UTF-8取出并按 ISO8859-1 编码
    // 这样可以保证弹出窗口中的文件名如果是中文的话就不会是乱码
    // 注意中文不要太多，最多支持17个中文，因为header有150个字节限制
    result = new String(clientFileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
    // 对文件名进行URL转义，防止中文乱码
    result = URLEncoder.encode(result, StandardCharsets.UTF_8);
    // 空格用 URLEncoder.encode 转义后会变成"+"
    // 所以要替换成"%20"，浏览器会解码回空格
    result = result.replace("+", "%20");
    // "+"用 URLEncoder.encode 转义后会变成"%2B"
    // 所以要替换成"+"，浏览器不对"+"进行解码
    return result.replace("%2B", "+");
  }

  /**
   * 根据文件扩展名设置上传文件的保存目录，不包含物理绝对路径，以"/"结尾
   *
   * @param extName 文件扩展名
   * @return 上传文件的保存目录的完整路径，包括物理路径
   */
  private static String setDirByExtName(String extName) {
    extName = extName.substring(1);
    String returnPath = "";
    switch (extName) {
      case "gif":
      case "jpg":
      case "jpeg":
      case "bmp":
      case "png":
      case "tiff":
      case "wmf":
      case "ico":
        returnPath += "image";
        break;
      case "txt":
      case "doc":
      case "docx":
      case "xls":
      case "xlsx":
      case "chm":
      case "pdf":
        returnPath += "document";
        break;
      case "zip":
      case "rar":
      case "7z":
        returnPath += "zip";
        break;
      case "avi":
      case "mp3":
      case "mp4":
      case "mpeg":
      case "wma":
      case "wav":
      case "flv":
      case "swf":
        returnPath += "media";
        break;
      default:
        returnPath += "other";
        break;
    }
    return returnPath + SEPARATOR;
  }

  private static String setDirByNow(String fatherDir) {
    String yearString = DateTimeUtil.getYearString(new Date());
    String monthString = DateTimeUtil.getMonthString(new Date());
    String dayString = DateTimeUtil.getDayString(new Date());
    return fatherDir + yearString + SEPARATOR + monthString + SEPARATOR + dayString + SEPARATOR;
  }

  /**
   * 检查上传的文件扩展名
   *
   * @param configDto 系统设置对象
   */
  private static boolean checkExt(String extName, UploadConfigVo configDto) {
    if (!StringUtils.hasLength(extName)) {
      return false;
    }
    String[] allowExt = configDto.getAllowedExt().split(",");
    for (int i = 0; i <= allowExt.length - 1; i++) {
      if (extName.equals(allowExt[i])) {
        return true;
      }
    }
    return false;
  }

  /**
   * 检查上传的文件的大小是否在允许范围内
   *
   * @param extName    文件扩展名
   * @param fileLength 文件大小
   * @param configDto  系统设置对象
   * @return 是否在允许范围内
   */
  private static boolean checkFileSize(String extName, Long fileLength, UploadConfigVo configDto) {
    double fileSize = (double) (fileLength / 1024L);
    /*
     if ("B".equalsIgnoreCase(unit)) {
     fileSize = (double) fileLength;
     } else if ("K".equalsIgnoreCase(unit)) {
     fileSize = (double) (fileLength / 1024L);
     } else if ("M".equalsIgnoreCase(unit)) {
     fileSize = (double) (fileLength / 1048576L);
     } else if ("G".equalsIgnoreCase(unit)) {
     fileSize = (double) (fileLength / 1073741824L);
     }
     */
    extName = extName.substring(1);
    switch (extName) {
      case "gif":
      case "jpg":
      case "jpeg":
      case "bmp":
      case "png":
      case "tiff":
      case "wmf":
      case "ico":
        return fileSize <= configDto.getAllowedSizeImg();
      case "txt":
      case "doc":
      case "docx":
      case "xls":
      case "xlsx":
      case "chm":
      case "pdf":
        return fileSize <= configDto.getAllowedSizeDoc();
      case "zip":
      case "rar":
      case "7z":
        return fileSize <= configDto.getAllowedSizeZip();
      case "avi":
      case "flv":
      case "mp3":
      case "mp4":
      case "mpeg":
      case "wma":
      case "wav":
      case "swf":
        return fileSize <= configDto.getAllowedSizeMedia();
      default:
        return false;
    }
  }

  /**
   * 根据完整文件名获取ContentType。支持本地文件，也支持http/https远程文件
   */
  private static String getContentType(String fileUrl) {
    String contentType;
    try {
      contentType = new MimetypesFileTypeMap().getContentType(new File(fileUrl));
    } catch (Exception e) {
      contentType = "";
    }
    return contentType;
  }

}
